サービスアカウントを切り替えて BigQuery のアクセス権限を制御してみた
こんにちは、みかみです。
やりたいこと
アプリなどから BigQuery にアクセスする場合に、アクセス制御するにはどうすれば良いのか知りたい
サービスアカウントを切り替えるということ
BigQuery のアクセス権は、アカウントに付与されたロールで制御可能です。 なお、BigQuery には SQL の GRANT 構文はなく、現在のところ、テーブル、ビュー、列、行単位でのアクセス制御はできません。
- データセットへのアクセスの制御 | BigQuery ドキュメント
- GCP Cloud IAM と BigQuery のデータセット、テーブル、View へのアクセス制御を確認してみた | Developers.IO
アプリなどからクライアントライブラリを使って BigQuery API 経由で BigQuery にアクセスする場合、アカウントキーを使用してサービスアカウント認証を実行します。
サービスアカウントにロールを付与し、アクセスするアカウントを切り替えることで BigQuery のアクセスが制御できるか確認してみます。
下図のようなイメージです。
サービスアカウントを作成
GCP 管理コンソールナビゲーションメニュー「IAM と管理」から、「サービスアカウント」を選択してサービスアカウントAを追加します。
「サービスアカウント名」と「サービスアカウントの説明」を入力し「作成」
アカウントの権限設定画面で「ロール」に「BigQuery 管理者」を選択し「続行」
「キーを作成」ボタンでアカウントキーの JSON ファイルをダウンロードして「完了」します。
サービスアカウントキーで BigQuery にアクセス
まず、認証情報の指定がない状態で、Python クライアントライブラリを使って BigQuery にアクセスしてみます。
Python 3.7.4 環境で、google-cloud-bigquery をインストールしました。
(test_account) [ec2-user@ip-10-0-43-239 ~]$ python --version Python 3.7.4 (test_account) [ec2-user@ip-10-0-43-239 ~]$ pip -V pip 19.0.3 from /home/ec2-user/test_account/lib64/python3.7/site-packages/pip (python 3.7) (test_account) [ec2-user@ip-10-0-43-239 ~]$ pip install google-cloud-bigquery Collecting google-cloud-bigquery (省略) Installing collected packages: six, google-resumable-media, protobuf, chardet, certifi, idna, urllib3, requests, googleapis-common-protos, pytz, pyasn1, pyasn1-modules, cachetools, rsa, google-auth, google-api-core, google-cloud-core, google-cloud-bigque ry Running setup.py install for googleapis-common-protos ... done Successfully installed cachetools-4.0.0 certifi-2019.11.28 chardet-3.0.4 google-api-core-1.16.0 google-auth-1.13.1 google-cloud-bigquery-1.24.0 google-cloud-core-1.3.0 google-resumable-media-0.5.0 googleapis-common-protos-1.51.0 idna-2.9 protobuf-3.11.3 pyasn1-0.4.8 pyasn1-modules-0.2.8 pytz-2019.3 requests-2.23.0 rsa-4.0 six-1.14.0 urllib3-1.25.8
クライアントライブラリを使用して API 経由で BigQuery にアクセスする場合、コードで認証情報を指定しないと、環境変数に設定されたデフォルトの認証情報でアクセスします。
現在、認証情報のデフォルトとなる、環境変数 GOOGLE_APPLICATION_CREDENTIALS は設定していない状態です。
(test_account) [ec2-user@ip-10-0-43-239 ~]$ echo $GOOGLE_APPLICATION_CREDENTIALS (test_account) [ec2-user@ip-10-0-43-239 ~]$
ためしに以下の認証情報指定なしの Python コードを実行してみます。
from google.cloud import bigquery query = ( 'SELECT name FROM `cm-da-mikami-yuki-258308.dataset_1.table_dogs`' 'WHERE id = 1 ') client = bigquery.Client() query_job = client.query(query) rows = query_job.result() for row in rows: print(row.name)
(test_account) [ec2-user@ip-10-0-43-239 ~]$ python test_select.py Traceback (most recent call last): File "test_select.py", line 3, in <module> client = bigquery.Client() File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/client.py", line 177, in __init__ project=project, credentials=credentials, _http=_http File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/client.py", line 226, in __init__ _ClientProjectMixin.__init__(self, project=project) File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/client.py", line 178, in __init__ project = self._determine_default(project) File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/client.py", line 193, in _determine_default return _determine_default_project(project) File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/_helpers.py", line 186, in _determine_default_project _, project = google.auth.default() File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/auth/_default.py", line 321, in default raise exceptions.DefaultCredentialsError(_HELP_MESSAGE) google.auth.exceptions.DefaultCredentialsError: Could not automatically determine credentials. Please set GOOGLE_APPLICATION_CREDENTIALS or explicitly create credentials and re-run the application. For more information, please see https://cloud.google.com/docs/authentication/getting-started
認証エラーになりました。
続いて、先ほど作成したサービスアカウントAのアカウントキーファイルパスをコード内で指定してみます。
from google.cloud import bigquery from google.oauth2 import service_account key_path = '/home/ec2-user/test_account/key_account_a.json' credentials = service_account.Credentials.from_service_account_file( key_path, scopes=["https://www.googleapis.com/auth/cloud-platform"], ) query = ( 'SELECT name FROM `cm-da-mikami-yuki-258308.dataset_1.table_dogs`' 'WHERE id = 1 ') #client = bigquery.Client() client = bigquery.Client( credentials=credentials, project=credentials.project_id, ) query_job = client.query(query) rows = query_job.result() for row in rows: print(row.name)
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select.py シェパード
正常にアクセスすることができました。
なお、認証情報はファイルパスではなく、JSON オブジェクトで渡すことも可能です。
Python コードを以下に書き換えて実行してみます。
from google.cloud import bigquery from google.oauth2 import service_account import json key_path = '/home/ec2-user/test_account/key_account_a.json' #credentials = service_account.Credentials.from_service_account_file( # key_path, # scopes=["https://www.googleapis.com/auth/cloud-platform"], #) service_account_info = json.load(open(key_path)) credentials = service_account.Credentials.from_service_account_info( service_account_info) query = ( 'SELECT name FROM `cm-da-mikami-yuki-258308.dataset_1.table_dogs`' 'WHERE id = 3 ') client = bigquery.Client( credentials=credentials, project=credentials.project_id, ) query_job = client.query(query) rows = query_job.result() for row in rows: print(row.name)
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_2.py 秋田犬
ファイルパスを指定した場合同様、正常にアクセスすることができました。
キーファイルをサーバ上に置くのはセキュリティ面が心配な場合など、ファイルは保護された別の場所に置いておいて、必要な時に read してアクセスするのも良いですね。
サービスアカウントを切り替えて BigQuery 操作権限を制御
もう一つ「BigQuery データ閲覧者」と「BigQuery ジョブユーザー」権限を持つ別のサービスアカウントBを作成しました。
Python 実行時のコマンドライン引数でファイル名を渡すように変更して、作成したアカウントBのキーファイルを指定して実行してみます。
from google.cloud import bigquery from google.oauth2 import service_account import argparse import os.path parser = argparse.ArgumentParser(description='select data') parser.add_argument('file', help='account key file') args = parser.parse_args() #key_path = '/home/ec2-user/test_account/key_account_a.json' key_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.file) credentials = service_account.Credentials.from_service_account_file( key_path, scopes=["https://www.googleapis.com/auth/cloud-platform"], ) query = ( 'SELECT id, name FROM `cm-da-mikami-yuki-258308.dataset_2.table_cats` ' 'ORDER BY id ' ) client = bigquery.Client( credentials=credentials, project=credentials.project_id, ) query_job = client.query(query) rows = query_job.result() for row in rows: print('{} : {}'.format(row.id, row.name))
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_arg.py key_account_b.json 1 : ベンガル 2 : ロシアンブルー 3 : 三毛猫
データが参照できることを確認して、次に、閲覧者権限しかないアカウントBのアカウントキーで、テーブルにデータを INSERT する以下のコードを実行してみます。
from google.cloud import bigquery from google.oauth2 import service_account import argparse import os.path parser = argparse.ArgumentParser(description='select data') parser.add_argument('file', help='account key file') args = parser.parse_args() key_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.file) credentials = service_account.Credentials.from_service_account_file( key_path, scopes=["https://www.googleapis.com/auth/cloud-platform"], ) query = ( "INSERT INTO `cm-da-mikami-yuki-258308.dataset_2.table_cats` VALUES (4, 'アビシニアン') " ) client = bigquery.Client( credentials=credentials, project=credentials.project_id, ) query_job = client.query(query) results = query_job.result()
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_insert.py key_account_b.json Traceback (most recent call last): File "test_insert.py", line 26, in <module> results = query_job.result() File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 3196, in result super(QueryJob, self).result(retry=retry, timeout=timeout) File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 818, in result return super(_AsyncJob, self).result(timeout=timeout) File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/api_core/future/polling.py", line 127, in result raise self._exception google.api_core.exceptions.Forbidden: 403 Access Denied: Table cm-da-mikami-yuki-258308:dataset_2.table_cats: User does not have bigquery.tables.updateData permission for table cm-da-mikami-yuki-258308:dataset_2.table_cats. (job ID: 839d4b96-52e1-4795-b988-f9c8dfe8c0af) -----Query Job SQL Follows----- | . | . | . | . | . | . | . | . | 1:INSERT INTO `cm-da-mikami-yuki-258308.dataset_2.table_cats` VALUES (4, 'アビシニアン') | . | . | . | . | . | . | . | . |
指定したアカウントキーのサービスアカウントBには INSERT 権限がないのでエラーになりました。
INSERT も可能な管理者権限を付与したアカウントAのキーファイルに切り替えて再実行してみると
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_insert.py key_account_a.json
エラーになることなく実行できました。
テーブルデータを確認してみます。
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_arg.py key_account_b.json 1 : ベンガル 2 : ロシアンブルー 3 : 三毛猫 4 : アビシニアン
ちゃんとデータが INSERT できていることが確認できました。
サービスアカウントを切り替えてデータセットアクセスを制御
dataset_1 と dataset_2 の2つのデータセットがあるとして、
- ユーザーBは dataset_1 と dataset_2 両方とも参照可能
- ユーザーCは dataset_1 は参照できるが dataset_2 は参照できない
場合を想定しました。
「BigQuery ジョブユーザー」権限しか持っていない、別のサービスアカウントCを作成します。
BigQuery 管理画面の「共有データセット」から、dataset_1 に、アカウントCの BigQuery データ閲覧権限を追加しました。
一方、dataset_2 には、アカウントCのアクセス権を追加しませんでした。
dataset_1、dataset_2 それぞれに対して、アカウントCのアクセスキーで SELECT を実行してみます。
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_ds1.py key_account_c.json 1 : シェパード 2 : シベリアンハスキー 3 : 秋田犬
dataset_1 には正常にアクセスできます。 dataset_2 のデータも SELECT してみると・・・
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_ds2.py key_account_c.json Traceback (most recent call last): File "test_select_ds2.py", line 26, in <module> rows = query_job.result() File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 3196, in result super(QueryJob, self).result(retry=retry, timeout=timeout) File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 818, in result return super(_AsyncJob, self).result(timeout=timeout) File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/api_core/future/polling.py", line 127, in result raise self._exception google.api_core.exceptions.Forbidden: 403 Access Denied: Table cm-da-mikami-yuki-258308:dataset_2.table_cats: User does not have permission to query table cm-da-mikami-yuki-258308:dataset_2.table_cats. (job ID: 10503ea7-dcb8-45bb-b219-8335cf4ee295) -----Query Job SQL Follows----- | . | . | . | . | . | . | . | . | 1:SELECT id, name FROM `cm-da-mikami-yuki-258308.dataset_2.table_cats` ORDER BY id | . | . | . | . | . | . | . | . |
アカウントCは dataset_2 の閲覧権限を持っていないので、permission エラーになりました。
dataset_2 閲覧権のあるアカウントBのキーファイルを指定して再実行してみると
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_ds2.py key_account_b.json 1 : ベンガル 2 : ロシアンブルー 3 : 三毛猫 4 : アビシニアン
アカウントキーを切り替えることで、付与された権限に従って BigQuery のアクセス制御ができることを確認できました。
まとめ(所感)
クライアントライブラリを使えばややこしい OAuth 2.0 の認証も自動でやってくれるので、アカウントキーファイルだけ意識しておけば良さそうです。
サービスアカウントへのロール付与と、BigQuery データセットの権限付与に気をつければ、サービスアカウントを切り替えることで、アプリなどからでも BigQuery の操作権限やデータセットのアクセス権限を簡単に制御することができました。